Skip to content

Release 1.8.0: staff area with cataloguing, circulation and plugin content, offline catalogue and a loans calendar - #47

Merged
fabiodalez-dev merged 62 commits into
mainfrom
feat/staff-catalog-offline-calendar
Oct 14, 2026
Merged

fabiodalez-dev merged 62 commits into
mainfrom
feat/staff-catalog-offline-calendar

Conversation

@fabiodalez-dev

@fabiodalez-dev fabiodalez-dev commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

What this adds

Pinakes Android 1.8.0 (versionCode 22) brings a native admin area for admin and staff accounts, an offline catalogue and a subscribable loans calendar. Each staff section appears only when the signed-in role is admin or staff and the server advertises it (Mobile API 1.7.0, fabiodalez-dev/Pinakes#477); on older servers the entries stay hidden and the app behaves as before.

Staff area

  • Cataloguing: ISBN/EAN scanner (CameraX + ZXing, no Google Play services) with checksum validation, double-read confirmation, batch mode and preview/duplicate/not-found sheets; manual records for items without an ISBN; full book editor with copies (own inventory codes, status, notes), shelf location, cover from URL/photo/gallery, label PDFs; authors and publishers with detail pages; genres; shelves; series; merge duplicates; CSV and LibraryThing import with resumable progress; missing covers and enrichment; digital file upload.
  • Circulation desk: counters, loans by status with search, loan detail with every action the server offers (approve, reject, pickup, return with copy outcome and penalty, renew, edit, reminder, receipt PDF/email), new loan with borrower search, book search or a scanned copy label and a per-user availability calendar, reservations queue, return by scanning a copy label (CODE 128 / CODE 39 / EAN-13), bulk extend and reminders. Creates send an Idempotency-Key, so a retry never lends a second copy. The whole section is hidden offline.
  • Plugin content: donation offers (Desiderata), archives with authorities and documents, periodicals desk (issue lookup, receive, claim, years, issues, subscriptions), book clubs (settings, workflow, members), FRBR works and expressions.

Offline catalogue

Room mirror of the whole catalogue kept current by /catalog/sync, plus the user's loans, reservations and wishlist. Search, book pages, My Library and Wishlist work offline; loan and reservation actions are hidden (not disabled) while offline and cached availability is shown as "last known".

Loans calendar

Profile → "Loans in your calendar": a private link with loans (start to due date) and reservations, copied as a sensitive clip and shown masked, with webcal opening, regenerate and turn off.

Other

  • Gender and tax code disappear when the library hides them.
  • The app sends its language (Accept-Language), so server labels and messages follow it; dates follow the language picked in the app.
  • The loan calendar fits phone screens; tab labels never wrap mid-word; raw network errors are never shown.
  • README gains a staff area gallery (24 screenshots).

Tests

  • 380 JVM unit tests, 0 lint errors, debug and R8 release builds.
  • 92 instrumented tests on an Android 15 emulator against a fake on-device server with fixtures, covering every staff flow, offline mode, the calendar sheet and phone-width layouts.
  • tools/e2e/live-smoke.sh drives the real app against a real Pinakes instance (19 steps, cleanup of everything it creates); STATUS.md explains how to run both suites.

Fixes #39
Fixes #32
Fixes #33

CameraX 1.4.1 (camera2, lifecycle, view) drives the preview and frame analysis; ZXing core 3.5.3 decodes EAN-13 in pure Java, so scanning needs no Google Play services.
…calendar

Mobile API 1.6.0 support without any UI change yet: the staff_catalog, catalog_sync and calendar_feed health flags (off on older servers); the user role kept in SessionStore from login and GET /me, with StaffAccess deciding who sees the staff area; StaffApi and StaffRepository for every /staff route (a 403 drops the stored role); tolerant models for lookups, ISBN lookups, books, authors, publishers, genres and shelves; 409 duplicate and conflict context kept in ApiResult.Failure.meta.

Pure, unit-tested logic: IsbnCodec (EAN-13 and ISBN-10 checksums, ISBN-10 to 13), ScanDebouncer (two identical reads, 3 s suppression, pause), StaffBookForm with StaffBookWrite (create body, true patch for edits that re-sends preserved contributors), SyncMerge (last change wins, full resync, weekly forced pass) and CalendarLinks.

Room schema v2 replaces the 40-row snapshot with the whole catalogue plus cached loans, reservations, wishlist and sync bookkeeping. CatalogRepository.syncCatalog() pages through GET /catalog/sync, applies the result in one transaction and only then stores the watermark; servers without the endpoint keep the old snapshot. Loans and wishlist are written through on every fetch and purged on sign-out or a new sign-in. The sync worker refreshes the personal lists too, the cache database is excluded from backups and the foreground observer also refreshes the role.
Search falls back to the cached catalogue (title, author, publisher and identifier match, the same facets by value, offset paging, title or newest order) under an offline banner. A book page is built from the cache with a notice; Library and Wishlist show the last lists the server sent.

Availability cannot be guaranteed offline, so loan and reservation affordances are hidden, never disabled: no request or reserve button, no loan calendar, no cancel actions, no wishlist toggle or reviews. Cached availability is never shown as current: the book page says when it was last known (sync time), status pills are hidden on cached cards and the cached Home shelf is labelled as recently added instead of available now. OfflinePolicy holds the decision and is unit-tested.
Profile shows "Loans in your calendar" when the server advertises calendar_feed and loans are on. The sheet creates the private ICS link, leads with Copy link (Google Calendar on Android cannot subscribe from the device; the hint explains the website route), opens it as webcal:// with the HTTPS link as fallback, and offers Regenerate and Turn off, both behind a confirmation because existing subscriptions stop updating.

Profile also shows "Staff area" for admin and staff users on servers with staff_catalog, and GET /me now refreshes the stored role.
Staff hub (Profile, or the scan button on the Catalog tab) for admin and staff users on servers with staff_catalog. Books: searchable list of every record with edit and delete (refused with a clear message while loans or reservations are active). One create/edit form serves scanned titles and records without an ISBN: live identifier validation, author and publisher pickers with add-new, genre and subgenre, media type, language/format/acquisition suggestions from the lookups, shelf and level with the next free position, copies (create) or add copies (edit), and a cover from a URL, a photo or a picked image (resized, EXIF-rotated JPEG under 2 MB, uploaded after the save). A 409 duplicate offers Open existing, Add copy or Change identifier. Authors, publishers, genres (tree, find-or-create) and shelves with levels have their own screens.

Scanner: CameraX preview with a viewfinder, ZXing EAN-13 decoding on alternate rotated frames, checksum validation and the double-read debouncer. Batch mode remembers copies, shelf, level and an opt-in auto-confirm; preview, duplicate and not-found sheets lead to add, add copies, edit details or create manually. The manual ISBN/EAN field is always visible for emulators and denied permission. CAMERA is requested only here, with a rationale and a settings fallback when it is permanently denied; camera.any is optional so devices without a camera still install the app. Without a connection the scanner says that scanning needs one.
…oans calendar

Bump the version and record the new features, their server requirement (Mobile API 1.6.0) and what is not yet verified on a device in STATUS.md and the Uwe parity notes.
The feed URL carries a bearer-equivalent token. Copy link now flags the clip as sensitive (ClipDescription.EXTRA_IS_SENSITIVE on Android 13+, the same extra key before) with a neutral label, so the clipboard preview and keyboards do not show or keep it, and the sheet shows only the host and the last six characters instead of the full URL. The URL is never logged.
Blank fields became null and the shared Json drops null keys, so the server kept the old value and a pseudonym, date, website or GND id could never be emptied. Every editable field is now always sent, trimmed, with a cleared field as an empty string, which the server stores as empty (dates and the GND id as NULL). RecordWritesTest checks the encoded bodies.
A search or pull-to-refresh during a page load dropped that page through the generation check but left loadingMore set, so loadMore() never ran again. reload() now clears the flag.
Picking a shelf without a level sent null shelf and level ids, which cleared the stored location without a word. A shelf without a level is now a validation error with a message under the level field (all four languages); nulls are sent only when the shelf is set to none, and a stored location without a level is left untouched instead of making the form invalid.

When the record saved but the cover upload failed, the screen navigated away and the error was lost. It now stays on the saved record (switching a create into an edit of the new id), keeps the pending cover so the next Save retries it, and says so.
Search: rows store a folded search key (NFD, combining marks removed, lower case; identifiers without dashes or spaces) and queries are folded the same way, so Città, Über or ÆRØ match regardless of accents and case. LIKE metacharacters in the query and the author/publisher filters are escaped with ESCAPE, and identifier-like queries drop their separators. Room schema v3 (destructive, refilled by the next full sync).

Sync: a page with has_more and no cursor now fails the run instead of applying a partial result and advancing the watermark. Clearing the cache (forget instance) bumps a generation under a write lock that every sync write also takes, so a run in flight can no longer write the old library's rows or watermark after the clear. The book detail ETag cache is concurrent.
Any 403 counted as a revoked role, so a WAF or proxy block wiped the stored role and closed the form. ApiResult.Failure now records whether its code came from the server's envelope, and only an envelope error.code of forbidden drops the role.
A ConnectivityObserver (system default-network callback, Hilt singleton) now feeds Home, Search, book detail, Library and Wishlist. When the device goes offline, screens already loaded online hide availability pills, the available-now claim, loan and reservation buttons, the loan calendar, cancel and remove actions at once; when it comes back they reload and the offline catalogue syncs. Unit tests cover each screen state.

A NETWORK failure never reaches the UI as its IOException text: resolvedMessage() and the new uiText()/serverMessage() helpers give a localized offline message, Library and Wishlist without a saved copy show an offline empty state, Home shows the same wording, staff screens map their load errors likewise, and the desiderata, archive and article sections of Catalog search are hidden when they fail only for lack of a connection. The author, publisher and genre pickers and lists get their own search placeholders.
Counts read "Copies: 1" style in every language instead of a plural noun after the number ("1 copies"): batch copies, added this session, copies added, available of total and the offline last-known line. The calendar sheet now says the feed carries loans (due dates, books ready for pickup, booked loans), not reservations.
The header kept the role of the profile loaded when the screen opened, so a downgraded admin still read Admin while the staff entries had already gone. It now shows the stored role, refreshed by every GET /me, and Profile re-reads GET /me quietly when it comes back to the foreground.
A staff create runs the whole admin pipeline (scraper hooks, cover download) and took over 30 s on a loaded server during the live smoke test: the app timed out and reported a failure for a record the server had already saved. The read timeout is now 60 s; the connect timeout is unchanged, so an unreachable server is still detected quickly.
The new staffe2e suite drives the real app (MainActivity, real Hilt graph, ViewModels, repositories and Room) against FakePinakes, a MockWebServer on the device loopback that answers JSON fixtures from androidTest/assets/fixtures and records every request. UiAutomator handles the system camera prompt; airplane mode is real.

Covered: staff entry only for admin/staff with staff_catalog (hidden for standard users, without the flag and after a server-side downgrade, header included); scanner manual ISBN to preview to add with the counter, duplicate to add copies, not found to the manual form, invalid code rejected without a lookup, camera denied keeps manual entry; manual form with a missing title or a shelf without a level sends nothing, an edit sends only the changed key; offline search banner with cached results and no availability or loan actions, offline detail with the last-known wording and no loan buttons, Library and Wishlist offline empty state without raw exception text, going offline after an online load hides the loan button and reconnecting restores it; calendar link shown masked, copied as sensitive, regenerate and turn off both confirm. 18 tests, all green on Android 15 together with the 22 existing ones.
tools/e2e/live-smoke.sh drives the installed app on an emulator with adb and uiautomator (tools/e2e/ui.py) like a user: onboarding, sign-in, staff hub, scan-add of an ISBN with two copies, a copy through the duplicate sheet, a record without ISBN, its shelf and level on edit (checking that nothing else changed), airplane-mode search and detail, the calendar link and its feed. Every step is checked through the Mobile API, and cleanup removes only what the run created (books, a calendar link that did not exist before, device tokens issued during the run) and clears the app. Instance URL and credentials come only from PINAKES_E2E_* variables; texts follow the device language through i18n/*.json. STATUS.md documents how to run it and the device tests.
The administrator can now hide the gender and tax code fields. The app reads it from GET /me (hidden_fields, the keys themselves omitted), from the registration schema (profile_fields.*.visible) and from /health (registration.show_*): a hidden field is not rendered in the profile editor and never sent in PATCH /me. Servers that send none of these keep both fields, as before. Registration never asked for them and still does not. Profile re-reads the schema with /me when it comes back to the foreground.

Tests: ProfileFieldVisibilityTest for the decision and the payloads; ProfileFieldsFlowTest on the device for hidden and shown fields, registration, and the birth-date picker fitting a 360dp screen.
Staff Authors and Publishers rows now open a detail page: a padded info card (books in the catalogue, life dates, nationality, website, GND id or the publisher's contacts, and created/updated when the server sends them) and the record's books from GET /staff/books?author_id= (publishers by name), drawn with the whole-cover book cards. Editing moved to the page's edit action. The catalogue filtered by an author gets the same kind of card above the results. The pending cover preview in the book form and the covers in My reviews now fit instead of cropping.
The loan request used Material's DatePickerDialog, which is a fixed 360dp wide with a capped height, and day cells were a fixed 40dp: on 320-360dp phones the month was clipped and cells ran into each other. The request is now a plain dialog sized to the screen (16dp margins, at most 420dp, scrollable), day cells fill their column (at most 44dp), weekday headers share the grid's spacing, and the legend wraps. When availability cannot be loaded the same grid lets the user pick any future day instead of a second date picker. CalendarLayoutTest checks the 7 weekday columns, that every day cell stays inside the screen and inside a 320dp column, and month navigation.
With database access configured, the smoke test hides gender and tax code in the settings table, waits for /health to report it, checks that /me omits them and that the app's profile editor no longer shows them, then restores the previous values. STATUS.md lists the new device tests.
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 645239a7-6be5-411a-a8a5-c8482b1e5c50
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Two issues should be fixed before release. The author and publisher editor can wipe a record after a failed load. The search screen gains a new connectivity collector every time a search is submitted, the sort changes or filters are cleared. The other inline notes are smaller.

Reviewed changes

This review covers the whole 1.7.0 release branch: the data layer, staff area, scanner, offline catalogue, calendar and the screen changes. Test fixtures were skimmed.

  • Staff area: adds role-gated catalogue management (books, authors, publishers, genres, shelves). Book edits send patch bodies, and a role is revoked only on the server's own forbidden code.
  • ISBN scanner: adds CameraX and ZXing EAN-13 scanning. A code needs two identical reads, then a 3 s suppression window applies. Includes batch mode and preview, duplicate and not-found sheets.
  • Offline catalogue: moves Room to a v3 schema. The delta sync on /catalog/sync uses a guarded watermark, falls back to the legacy snapshot on servers without the endpoint, and serves offline search with a folded search key. Loans and wishlist are cached too, and loan actions are hidden offline through ConnectivityObserver.
  • Loans calendar: adds create, rotate and revoke for the private ICS feed. The link is shown masked and copied as a sensitive clip.
  • Misc: hides profile fields the library hides, raises the read timeout to 60 s, bumps the version to 20 / 1.7.0, and adds device tests and the live smoke script.

The sync path holds up. The watermark is stored only after a successful apply, cacheGeneration stops a run still in flight when the instance is forgotten from writing back, and LIKE input is escaped. A few lower-confidence leftovers are not flagged inline: cached_books.isWanted is per-user and survives logout, and the cached wishlist and loans only refresh on a full list fetch, not after add or remove.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread app/src/main/java/com/pinakes/app/ui/screens/staff/StaffRecordsScreens.kt Outdated
Comment thread app/src/main/java/com/pinakes/app/ui/screens/search/SearchViewModel.kt Outdated
Comment thread app/src/main/java/com/pinakes/app/ui/screens/search/SearchViewModel.kt Outdated
Comment thread app/src/main/java/com/pinakes/app/ui/screens/search/SearchViewModel.kt Outdated
Comment thread app/src/main/java/com/pinakes/app/ui/screens/profile/ProfileViewModel.kt Outdated
Comment thread app/src/main/java/com/pinakes/app/ui/screens/staff/StaffBookEditViewModel.kt Outdated
Comment thread app/src/main/java/com/pinakes/app/ui/screens/staff/scan/StaffScanViewModel.kt Outdated
When GET /staff/authors/{id} or /staff/publishers/{id} failed, the editor kept a blank placeholder with Save enabled; since saves send every field and an empty string clears it, saving wiped the stored record. The placeholder is now marked as not loaded: it shows the error with Retry only (no fields, no Save), edits and submit ignore it, and a late answer for an editor already closed is dropped.
submitSearch(), setSort() and clearFilters() each started another connectivity collector that was never cancelled, so one reconnect re-ran the search once per earlier interaction. Only the collector started in init remains. ConnectivityCollectorTest checks that a single collector is left after repeated searches, sorts and filter resets.
Creating, regenerating or turning off the calendar link, saving the profile and changing the password showed the IOException text when offline. They now use the server's own message or the localized offline wording.
"Add copy" sent the form's copy count, which in edit mode is the edited record's own total: an ISBN change that collided with another book added that many copies to it. Editing now adds exactly one; creating keeps the copies the user asked for.
The 3 s window started when a code was accepted, but the lookup and the open sheet usually last longer, so after Skip or Add the book still under the camera reopened the same sheet. resume(now) restarts the window for the last accepted code; other codes are not held back.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new Tier 2 screens have two behaviour bugs. Fix them before the server turns these flags on. Scanning an issue with the camera can't identify the issue, because the barcode add-on is dropped. Saving a book club workflow also strips any state flag the app doesn't model. A stale STATUS.md note is inline too.

Reviewed changes

This review covers the one commit added since the last pullfrog review at 80d7173.

  • Periodicals desk: added a scanner and manual code entry that look up an issue (scan-lookup), then receive or claim it depending on its status. Below the scanner is a searchable, paged list of mastheads. Each masthead page has expandable years with their issues, plus forms to add an issue or a year and to create or edit a subscription.
  • Book clubs: added a club list with a create sheet. Each club page has settings, a workflow editor (at least two states, which can be reordered, added or removed, each with voting/current/archived flags), member role and status chips, adding a member by user search, and delete.
  • FRBR works: added a work search (up to 100 results) and a create sheet. Each work page has an edit form, add/edit/delete for expressions, and linking or unlinking books.
  • Shared scanner and plumbing: moved the copy scanner's camera viewfinder into ScannerBox. Added StaffTier2Api/StaffTier2Repository (every call goes through staffCall), the pure Tier2Forms, and new routes and hub rows, each gated on its own area. StaffAreas.anyPlugin now covers the three new areas.
  • Tests and i18n: added Tier2FormsTest (7 tests), Tier2FlowTest (6 tests) and their fixtures, plus about 135 keys in each of the four languages. All four files have the same keys and matching placeholders, and none of them contain an em dash.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread app/src/main/java/com/pinakes/app/data/staff/Tier2Forms.kt Outdated
Comment thread STATUS.md Outdated
The live smoke looks up an unknown code at the periodicals desk (read-only: the bridge offers no deletes), and creates a book club and an FRBR work in the app, removing them in cleanup; long forms scroll to their Save button. STATUS and the Uwe parity notes now describe the periodicals desk, book club management and FRBR works as native, with the 19/19 live run.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues in this commit, and it fixes the stale STATUS.md note from the last review. Two findings from the review at b8db4c6 are still open because this commit changes no app code: the camera path drops the periodical's EAN add-on, and workflow saves strip state flags the app doesn't model. STATUS.md now says an arriving issue can be scanned, but with the camera that only works after the add-on fix.

Reviewed changes

This review covers the one commit added since the last pullfrog review at b8db4c6. It changes only docs and the live smoke script.

  • STATUS.md and parity notes: rewrote the Tier 2 entry to describe the periodicals desk, book clubs and FRBR works as shipped, each behind its own flag, and updated the Italian 1.8.0 summary in docs/UWE-PARITY.md to match. The new counts (377 unit tests, 92 device tests) match the @Test totals in app/src/test and app/src/androidTest.
  • Live smoke Tier 2 steps: added periodicals_desk, which first checks through the API that a fixed code has no match and then looks it up in the app without changing anything. Also added club_create and work_create, which create a record in the app and confirm it through the API. Each step is skipped while its flag is off. Every S[...] key they use exists in i18n/en.json, and the API paths match StaffTier2Api.
  • Cleanup: clubs and works are looked up again by their run-unique names, so a step that fails partway still gets swept. Each one is then deleted by id.

Pullfrog  | Fix it ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Server labels and error messages followed the installation's language, so an English app showed "Prestato" next to English text. The interceptor now adds Accept-Language with the language picked in Profile (or the device one) unless the request sets it; Mobile API 1.7.0 answers in it when the installation has that language and keeps its own otherwise.
At 360 dp the Reservations tab wrapped mid-word ("Reservation" / "s"), and German and Italian labels are just as long. The tabs are now a scrollable row with single-line labels, like the circulation desk.
Adds 24 screenshots of the 1.8.0 staff area (circulation desk, cataloguing and plugin content), captured on Android 15 at 360 dp against a local instance with demo data and resized to 720 px, and lists the staff area, circulation desk, offline catalogue and loans calendar in the features table.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues in the two new commits. Two findings from the review at b8db4c6 are still open, because these commits don't touch that code: the camera path drops the periodical's EAN add-on, and saving a workflow strips state flags the app doesn't model.

Reviewed changes

This review covers the two commits added since the last pullfrog review at 9be8401.

  • App language on every request: AuthInterceptor now adds Accept-Language from AppLocale.current() before it handles the no-auth marker. Public calls and the long-timeout client get the header too, since that client is derived from the same okHttpClient. A header set on the request itself wins, and nothing is sent for a blank or und tag. AcceptLanguageTest covers all three cases.
  • My Library tabs: replaced TabRow with ScrollableTabRow(edgePadding = 0.dp) and made the labels single-line, so "Reservations" / "Vormerkungen" no longer wrap mid-word. The tabs now sit at their natural width, starting from the left, instead of filling the row evenly.

One low-confidence note, not raised inline because it depends on the server: the HTTP cache is keyed by URL only and isn't cleared when the language changes. If the server's ETag stays the same across languages, a 304 after a language switch would replay the response in the old language until the data changes.

Pullfrog  | Fix it ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

@fabiodalez-dev fabiodalez-dev changed the title Release 1.7.0: staff cataloguing with ISBN scanning, offline catalogue and a loans calendar Release 1.8.0: staff area with cataloguing, circulation and plugin content, offline catalogue and a loans calendar Oct 14, 2026
The profile entry and the hub title read "Area staff" / "Staff area"; they now say "Area admin" (Admin area, Adminbereich, Espace admin), and the README follows.
The book page is its own destination above the tabs, so the Home/Catalogue/Library/Wishlist/Profile bar vanished there. The selected tab now lives in the nav host: the book page shows the same bar with the tab it was opened from, and a tap returns to that tab.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues in the new commit. One minor note is inline: the remembered tab now survives a logout. Two findings from the review at b8db4c6 are still open, because this commit doesn't touch that code.

Reviewed changes

This review covers the one commit added since the last pullfrog review at d8f9b3a.

  • Bottom navigation on the book page: moved the selected tab out of MainScaffold and into PinakesNavHost as mainTab, so BookDetailScreen can show the same PinakesBottomBar with the tab it was opened from. Tapping a tab sets mainTab and pops back to MAIN_GRAPH. When the active tab is hidden, MainScaffold now falls back to Home through a derived shownTab instead of writing to local state.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread app/src/main/java/com/pinakes/app/ui/navigation/PinakesNavHost.kt
The copy code, ISBN and periodical issue fields kept their text in the view model's state flow, so a keyboard-wedge reader (or a fast typist) that sends the whole label at once lost characters: LIB-239 arrived as L and the desk answered that no copy has that code. The text now stays in the screen and the view model only receives it on search.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. Three findings from earlier reviews are still open, because this commit doesn't touch that code. Two are from the review at b8db4c6: the camera drops the periodical's EAN add-on, and workflow saves strip state flags. The third is from 8722971: mainTab survives a logout.

Reviewed changes

This review covers the one commit added since the last pullfrog review at 8722971.

  • Scan code fields keep every character: the ISBN scanner, copy scan and periodicals desk used to keep the manual field's text in the view model's StateFlow. Each field now keeps its own text in a screen-local rememberSaveable, so a keyboard-wedge reader that types a whole label at once no longer loses characters on the per-keystroke round trip. submitManual(input) now returns whether the code was taken, and the field is cleared only in that case. That keeps the old behaviour of leaving the text in place while a lookup or sheet is open. The length caps stay in the screen (CopyCodeCodec.MAX_LENGTH, 30 for issues). PinakesTextField is single-line by default, so the reader's trailing Enter still triggers the Search action. No test or caller still refers to the removed manualInput/onManualInput/onManual members.

Pullfrog  | Fix it ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

A reader who has not borrowed the title saw the reviews section with no way to write one and no reason why. Below the reviews the page now says the book can be reviewed once it has been borrowed, in the four app languages.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. Three findings from earlier reviews are still open, because this commit doesn't touch that code. Two are from the review at b8db4c6: the camera drops the periodical's EAN add-on, and saving a workflow strips state flags. The third is from 8722971: mainTab survives a logout.

Reviewed changes

This review covers the one commit added since the last pullfrog review at 1b967c4.

  • Hint for readers who can't review yet: ReviewsBody now shows reviews_borrow_first when the editor is closed, the reader has no review yet, and neither the server's can_review nor the hasRead fallback allows a review. Before, no button appeared and nothing explained why. The key is in all four languages and none of them contain an em dash. The text carries a reviews-borrow-first test tag, but no device test uses it yet.

Pullfrog  | Fix it ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

The selected tab moved to the nav host for the book page's bottom bar, so it outlived MAIN_GRAPH: logging out from Profile and back in (another account, another library) reopened on Profile. Logout and any non-authenticated state now reset it to Home.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes

This run covers the one commit pushed since the last pullfrog review at 60c5e4a.

  • New sessions open on Home: onLoggedOut now resets mainTab to PinakesTab.Home. A new LaunchedEffect(authState) also resets it for any state other than Authenticated, which covers an expired or revoked token and a forgotten instance. SessionStore seeds authState synchronously from prefs, so the effect does not clear a tab that rememberSaveable restores on a cold start while the user is signed in. This addresses the mainTab logout thread raised at 8722971.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

ZXing reports an EAN's 2/5-digit add-on only as result metadata, and the analyzer passed on the 13 digits alone, so a camera scan could match the masthead but never the issue. The periodicals desk now asks for the add-on to be appended (the issue code scan-lookup expects); the book scanner keeps ignoring it, since on an ISBN the 5-digit add-on is a price. EanAddOnDecodeTest decodes a real masthead + add-on label from its luminance plane both ways.
The draft kept only voting, current and archived and sent only those three, so renaming a label stripped every other flag the server stored (votable on proposed, or any flag a later plugin version adds). Each state now carries its original flags object and the editor overwrites only the three it controls.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues. Both remaining b8db4c6 findings are fixed. One minor suggestion is inline.

Reviewed changes

This run covers the two commits pushed since the last pullfrog review at e52bdfb.

  • Camera scans keep the issue add-on: BarcodeAnalyzer takes appendEanAddOn, and codeOf appends ZXing's UPC_EAN_EXTENSION metadata to the 13 digits. The flag goes through CameraPreview and ScannerBox, and only the periodicals desk turns it on. The book scanner and copy scan still read just the 13 digits. EanAddOnDecodeTest decodes a real luminance-plane fixture and gets both results.
  • Workflow saves keep unmodelled flags: WorkflowStateDraft.serverFlags stores the state's original flags object. toJson writes those flags first and then sets voting/current/archived on top, so flags like votable survive a save. Tier2FormsTest covers both cases: the stored flags are kept, and the editor's own flags override them.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

…a masthead

ZXing reads the add-on only on some frames, so two bare 13-digit reads could confirm the masthead before the read with the issue won. On the desk a confirmed 977 masthead is now held for 800 ms: a read with its add-on is taken at once, a bare read right after an add-on read counts as that issue, and a masthead with no add-on is still looked up when the hold ends.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues. The add-on finding is fixed. One minor suggestion is inline.

Reviewed changes

This run covers the one commit pushed since the last pullfrog review at 07e4313.

  • The periodicals desk waits for the issue add-on: the new pure IssueScanResolver wraps ScanDebouncer. A bare 977… read that comes right after a read with its add-on counts as that issue. A confirmed bare masthead is held for 800 ms, and a read with its add-on during that time is taken at once. A masthead with no add-on is still looked up when flush runs after the hold. The desk VM runs a single holdJob for the hold, and lookup cancels it. IssueScanResolverTest covers alternating frames, the held masthead, a book EAN that is never held, and the paused state.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread app/src/main/java/com/pinakes/app/data/staff/IssueScanResolver.kt
…a denied camera

1.8.0-rc.1 shipped as versionCode 21, so the stable build needs 22 to install over it as an update. The camera-denied scanner test now revokes the permission itself instead of depending on what the emulator was left with.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No critical issues. There are two minor suggestions inline: one test fix and one stale doc line.

Reviewed changes

This run covers the one commit pushed since the last pullfrog review at 4da43cc.

  • Bumped versionCode to 22: the stable 1.8.0 build now installs over the 1.8.0-rc.1 build, which used 21.
  • Camera-denied scanner test revokes the camera first: cameraDeniedKeepsManualEntryAndExplainsWhy calls UiAutomation.revokeRuntimePermission(…, CAMERA) before launch().

The open IssueScanResolver.kt:39 thread from the previous review is not touched by this commit, so it stays open.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread app/src/androidTest/java/com/pinakes/app/staffe2e/ScannerFlowTest.kt Outdated
Comment thread STATUS.md Outdated
…r the camera

The held-masthead path returned the add-on read without asking the debouncer, so after receive, claim or dismiss two bare frames plus one add-on frame reopened the same issue. The resolver now drops any read the debouncer still suppresses, or that is the bare masthead of it.
Revoking a granted permission from inside the test kills the instrumentation process, which shares the app's UID, and the rest of the suite with it. The test now skips with the adb command that resets the permission, the README run steps include it, and STATUS.md names versionCode 22.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

The new suppression check in IssueScanResolver breaks camera lookups for magazines that have no add-on. Details are inline.

Reviewed changes

This run covers the two commits pushed since the last pullfrog review at ea339a9.

  • The desk no longer reopens the issue it just handled: IssueScanResolver.offer drops any read that ScanDebouncer.suppressedAt(now) (new) starts with, so neither the issue nor its bare masthead is looked up again while the magazine stays under the camera. IssueScanResolverTest.theIssueJustHandledIsNotLookedUpAgainAfterResume covers it.
  • The camera-denied test skips instead of revoking: cameraDeniedKeepsManualEntryAndExplainsWhy now uses assumeTrue to skip when CAMERA is already granted, and the README lists the adb shell pm revoke command to run first. This fixes the process-kill finding.
  • Updated STATUS.md to versionCode 22.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using claude-opus-5-5 | 𝕏

Comment thread app/src/main/java/com/pinakes/app/data/staff/IssueScanResolver.kt Outdated
The held masthead is the debouncer's last accepted code, so the post-resume check matched its own next frame and cleared the hold: a magazine with no add-on was never looked up from the camera while it stayed in view. The check now skips the held masthead, and the no-add-on test feeds frames during the hold.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found. The resolver fix restores camera lookups for magazines without an add-on.

Reviewed changes

This run covers the one commit pushed since the last pullfrog review at e4ce70e.

  • Kept a held masthead alive while its own frames keep arriving: IssueScanResolver.offer now skips the suppression early return when suppressed == pending. The magazine's next bare frames fall through to ScanDebouncer, which ignores them without clearing the hold, so flush still looks up a title-only masthead. Post-resume suppression is unchanged because pending is null after a lookup. aMastheadWithNoAddOnIsStillLookedUpAfterTheHold now feeds frames during the hold and asserts holding.

Pullfrog  | View workflow run | Using claude-opus-5-5 | 𝕏

@fabiodalez-dev
fabiodalez-dev merged commit f0a712a into main Oct 14, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant